package pir

import (
	"fmt"
	"runtime"
	"runtime/debug"
	"time"
)

type PIR interface {
	Name() string

	PickParams(N, d, n, logq uint64) Params
	PickParamsGivenDimensions(l, m, n, logq uint64) Params

	GetBW(info DBinfo, p Params)

	Init(info DBinfo, p Params, seed128 [16]byte) State
	InitCompressed(info DBinfo, p Params, seed128 [16]byte) (State, CompressedState)
	DecompressState(info DBinfo, p Params, comp CompressedState, seed128 [16]byte) State

	Setup(DB *Database, shared State, p Params) (State, Msg)
	FakeSetup(DB *Database, p Params) (State, float64) // used for benchmarking online phase

	Query(i uint64, shared State, p Params, info DBinfo) (State, Msg)

	QueryOffline(shared State, p Params, info DBinfo) (State, State)
	QueryOnline(i uint64, offline_query State, p Params, info DBinfo) Msg

	Answer(DB *Database, query MsgSlice, server State, shared State, p Params) Msg

	Recover(i uint64, batch_index uint64, offline Msg, query Msg, answer Msg, shared State, client State,
		p Params, info DBinfo) uint64
	RecoverVec(i uint64, batch_index uint64, offline Msg, query Msg, answer Msg, shared State, client State,
		p Params, info DBinfo) []uint64
	Reset(DB *Database, p Params) // reset DB to its correct state, if modified during execution
}

func OfflineSetup(pi PIR, DB *Database, p Params, seed128 [16]byte) (State, Msg, State) {
	fmt.Printf("Executing %s\n", pi.Name())
	debug.SetGCPercent(100) // why

	fmt.Println("DB.Data.Rows : ", DB.Data.Rows, " DB.Info.Ne : ", DB.Info.Ne, " DB.Data.Cols : ", DB.Data.Cols)
	fmt.Println("Setup, initializing the random matrix A..")
	A := pi.Init(DB.Info, p, seed128)

	fmt.Println("Setup, computing the DB*A...")
	start := time.Now()
	server_hint, client_hint := pi.Setup(DB, A, p)
	printTime(start)
	//fmt.Println("Client offline download: client_hint.Size() : ", Hint_client.Size())
	comm := float64(client_hint.Size()*uint64(p.Logq)) / float64(8.0*1024.0*1024)
	fmt.Printf("\t\tClient offline download: %f MB\n", comm)
	runtime.GC()
	return A, client_hint, server_hint
}
func Query(pi PIR, p Params, info DBinfo, A State, i uint64) (State, Msg) {

	fmt.Println("Building query...")

	start := time.Now()
	sk, query := pi.Query(i, A, p, info)

	printTime(start)
	//fmt.Println("Client query.Size() : ", query.Size())
	comm := float64(query.Size()*uint64(p.Logq)) / float64(8.0*1024.0) // Z_q ^ m
	fmt.Printf("\t\tClient online upload: %f KB\n", comm)
	runtime.GC()
	return sk, query
}
func QueryOffline(pi PIR, p Params, info DBinfo, A State) (State, State) {
	fmt.Println("Offline building query material...")
	start := time.Now()
	sk, Enc0 := pi.QueryOffline(A, p, info)

	fmt.Println("Offline stage:")
	printTime(start)
	runtime.GC()
	return sk, Enc0
}
func QueryOnline(pi PIR, p Params, info DBinfo, offline_query State, i uint64) Msg {
	fmt.Println("Building query...")
	start := time.Now()
	query := pi.QueryOnline(i, offline_query, p, info)
	fmt.Println("Online stage:")
	printTime(start)
	fmt.Println("Client query.Size() : ", query.Size())
	comm := float64(query.Size()*uint64(p.Logq)) / float64(8.0*1024.0) // Z_q ^ m
	fmt.Printf("\t\tClient online upload: %f KB\n", comm)
	runtime.GC()
	return query
}

func Answer(pi PIR, DB *Database, p Params, server_state State, A State, query Msg) Msg {
	fmt.Println("Answering query...")
	start := time.Now()
	answer := pi.Answer(DB, MakeMsgSlice(query), server_state, A, p)
	printTime(start)
	//fmt.Println("answer.Size() : ", answer.Size())
	comm := float64(answer.Size()*uint64(p.Logq)) / float64(8.0*1024.0)
	fmt.Printf("\t\tServer online download: %f KB\n", comm)
	runtime.GC()
	pi.Reset(DB, p)
	return answer
}

func Recover(pi PIR, p Params, info DBinfo, i uint64, H Msg, query Msg, answer Msg, A State, client_state State) uint64 {
	fmt.Println("Reconstructing...")
	start := time.Now()

	val := pi.Recover(i, uint64(0), H,
		query, answer, A,
		client_state, p, info)
	printTime(start)
	return val

}
func RecoverVec(pi PIR, p Params, info DBinfo, i uint64, H Msg, query Msg, answer Msg, A State, client_state State) []uint64 {
	fmt.Println("Reconstructing...")
	start := time.Now()

	vals := pi.RecoverVec(i, uint64(0), H,
		query, answer, A,
		client_state, p, info)
	printTime(start)
	return vals

}

func Verify(DB *Database, i uint64, val uint64) {
	fmt.Println("DB.GetElem(", i, "): ", DB.GetElem(i), " val: ", val)
	if DB.GetElem(i) != val {
		fmt.Printf("Batch %d (querying index %d -- row should be >= %d): Got %d instead of %d\n",
			0, i, DB.Data.Rows/4, val, DB.GetElem(i))
		panic("Reconstruct failed!")
	} else {
		fmt.Println("Success!")
	}
}

func VerifyVec(DB *Database, i uint64, vals []uint64) {
	fmt.Println("DB.GetElem(", i, "): ", DB.GetElemVec(i), "\n vals:           ", vals)
	var_temp := DB.GetElemVec(i)
	for i := uint64(0); i < uint64(len(var_temp)); i++ {
		if var_temp[i] != vals[i] {
			fmt.Printf("Batch %d (querying index %d -- row should be >= %d): Got %d instead of %d\n",
				0, i, DB.Data.Rows/4, vals, DB.GetElemVec(i))
			panic("Reconstruct failed!")
		}
	}

	fmt.Println("Success!")
}

func VerifyStr(str []string, i uint64, logp uint64, vals []uint64) {
	fmt.Println("str(", i, "): ", str[i], "\nvals:       ", Uint64ToStringByBits(vals, logp))
	var_temp := Uint64ToStringByBits(vals, logp)

	if var_temp != str[i] {
		panic("Reconstruct failed!")
	}

	fmt.Println("Success!")
}

func Reconstruct(logp uint64, vals []uint64) {
	fmt.Println("vals:       ", Uint64ToStringByBits(vals, logp))
}
